/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor Boston, MA 02110-1301,  USA
 */
 
/**
 * @version center.c 0.0.1
 * @package Comoro
 * @author Dr.NP <np@bsgroup.org>
 * 
 * Center server main file
 */

#include "bspd.h"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <event.h>
#include <pthread.h>

#ifdef HAVE_MALLOC_H
#ifndef __OpenBSD__
#include <malloc.h>
#endif
#endif

extern struct conf setting;
static unsigned long inited_time;
static pthread_mutex_t center_lock;
struct global_client_item *global_client_list;
struct base_server_info *base_server_list;
int ** base_client_list;
int total_clients, max_client_id;
int global_client_list_size;
int *base_client_list_size;
int total_init;
int total_regon, total_regoff;

/* Initializer */
int center_init()
{
	// Init db
	db_mysql_init();
	
	// Init memory pool
	mempool_init();
	
	// Init lua runner
	runner_init();
	
	// Record start timestamp
	inited_time = (unsigned long) time((time_t *) NULL);

	// Init global client pool	
	total_init = (bridge_get_mode() == BRIDGE_MODE_UNITED) ? 1 : BASE_CLIENT_BASE_SIZE;

	global_client_list = malloc(GLOBAL_CLIENT_POOL_BASE_SIZE * sizeof(struct global_client_item));	
	memset(global_client_list, 0xFF, GLOBAL_CLIENT_POOL_BASE_SIZE * sizeof(struct global_client_item));
	global_client_list_size = GLOBAL_CLIENT_POOL_BASE_SIZE;

	base_server_list = malloc(BASE_CLIENT_BASE_SIZE * sizeof(struct base_server_info));
	memset(base_server_list, 0, BASE_CLIENT_BASE_SIZE * sizeof(struct base_server_info));

	base_client_list = malloc(total_init * sizeof(int *));
	memset(base_client_list, 0, total_init * sizeof(int *));
	base_client_list_size = malloc(total_init * sizeof(int));
	memset(base_client_list_size, 0, total_init * sizeof(int));

	if (BRIDGE_MODE_CENTER == bridge_get_mode())
	{
		// Init network
		struct conn_server *c = (struct conn_server *) malloc(sizeof(struct conn_server));

		c->srv_addr = inet_addr(setting.center_srv_listen_addr);
		c->srv_port = setting.center_srv_port;
		c->listen_queue_length = 64;
		c->nonblock = 1;
		c->tcp_nodelay = 1;
		c->tcp_reuse = 1;
		c->tcp_send_buffer_size = 16384;
		c->tcp_recv_buffer_size = 16384;
		c->tcp_timeout = 3;
		c->on_data_callback = center_on_read;
		c->on_close_callback = center_on_close;

		// A new TCP server
		conn_tcp_server_new(c);

		log_add("Center server cluster initialized");
	}

	total_clients = 0;
	max_client_id = -1;
	total_regon = 0;
	total_regoff = 0;
	
	return RTN_SUCCESS;
}

/* Get center initialize time */
unsigned long center_init_time()
{
	return inited_time;
}

/* Read data from base server */
void center_on_read(int connector_id, void *data, unsigned int len)
{
	int base_id = (connector_id >= 0) ? connector_id : 0;

	unsigned int data_remain = len;
	unsigned int plen;
	unsigned int spa;
	int **tmp_list;
	int *tmp_array;
	struct base_server_info *tmp_info;

	while (connector_id >= total_init)
	{
		// Enlarge base list
		tmp_list = realloc(base_client_list, 2 * total_init * sizeof(int *));
		tmp_array = realloc(base_client_list_size, 2 * total_init * sizeof(int));
		tmp_info = realloc(base_server_list, 2 * total_init * sizeof(struct base_server_info));

		if (tmp_list && tmp_array)
		{
			memset(tmp_list + total_init * sizeof(int *), 0, total_init * sizeof(int *));
			memset(tmp_array + total_init * sizeof(int), 0, total_init * sizeof(int));
			memset(tmp_info + total_init * sizeof(struct base_server_info), 0, total_init * sizeof(struct base_server_info));
			
			base_client_list = tmp_list;
			base_client_list_size = tmp_array;
			base_server_list = tmp_info;
			
			total_init *= 2;
		}

		else
		{
			return;
		}
	}

	//package_output(data, len);
	// Split data into commands
	while (data_remain > 0)
	{
		if (data_remain >= 4)
		{
			// Read package length
			plen = get_u_int(data + len - data_remain);
			if (plen > SGN_RANGE)
			{
				// Status information
				if (plen == SGN_CMD_STATUS && data_remain >= STATUS_PACKAGE_LENGTH)
				{
					center_store_base_info(base_id, data + 4);
					data_remain -= STATUS_PACKAGE_LENGTH;
					
					continue;
				}

				else if (plen == SGN_CLIENT_REG && data_remain >= 20 + MAX_USERNAME_LENGTH)
				{
					unsigned long long user_id;
					int runner_id;
					int global_id;
					
					spa = get_u_int(data + len - data_remain + 4);
					user_id = get_u_ll(data + len - data_remain + 8);
					runner_id = (int) get_u_int(data + len - data_remain + 16);

					global_id = center_add_client(base_id, (int) spa);
					mempool_add_client(global_id, user_id, data + 20);
					mempool_set_client_runner(global_id, runner_id);

					data_remain -= 20 + MAX_USERNAME_LENGTH;
				}
				
				// Special command
				else if (data_remain >= 8)
				{
					// 8 bytes per command
					spa = get_u_int(data + len - data_remain + 4);
					center_special_command(base_id, plen, spa);
					data_remain -= 8;

					continue;
				}

				else
				{
					// Command error
					data_remain = 0;
					fprintf(stderr, "Package error on cmd %X\n", plen);
					
					break;
				}
			}

			else
			{
				if (4 <= plen && data_remain >= plen)
				{
					// Valid package, get it
					spa = get_u_int(data + len - data_remain + 4);
					center_process_command(base_id, (int) spa, (void *) data + len - data_remain + 8, (int) plen - 4);
					data_remain -= plen;

					continue;
				}

				else
				{
					// Half package
					data_remain = 0;

					break;
				}
			}
		}
	}
	
	return;
}

/* On connector close */
void center_on_close(int connector_id)
{
	return;
}

/* Get base server id by given client id */
struct global_client_item * center_get_base_info(int global_id)
{
	return global_id >= global_client_list_size ? NULL : &global_client_list[global_id];
}

/* Add a new client */
int center_add_client(int base_id, int client_id)
{
	int id = center_get_free_global_id();
	int *tmp_array;

	if (id < 0)
	{
		return RTN_GENERAL;
	}

	global_client_list[id].base_id = base_id;
	global_client_list[id].client_id = client_id;

	// Insert into base client list
	if (!base_client_list[base_id])
	{
		// A new pool
		tmp_array = (int *) malloc(GLOBAL_CLIENT_POOL_BASE_SIZE * sizeof(int));
		
		if (!tmp_array)
		{
			return RTN_GENERAL;
		}

		memset(tmp_array, 0xFF, GLOBAL_CLIENT_POOL_BASE_SIZE * sizeof(int));
		base_client_list[base_id] = tmp_array;
		base_client_list_size[base_id] = GLOBAL_CLIENT_POOL_BASE_SIZE;
	}

	// Try insert
	while (client_id >= base_client_list_size[base_id])
	{
		// Enlarge base item
		tmp_array = realloc(base_client_list[base_id], 2 * base_client_list_size[base_id] * sizeof(int));

		if (!tmp_array)
		{
			memset(tmp_array + base_client_list_size[base_id] * sizeof(int), 0xFF, base_client_list_size[base_id] * sizeof(int));
			base_client_list[base_id] = tmp_array;
			base_client_list_size[base_id] *= 2;
		}
	}

	base_client_list[base_id][client_id] = id;
	
	total_clients ++;
	total_regon ++;
	
	fprintf(stderr, "Base %d Client %d added into global %d\n", base_id, client_id, id);
	
	return id;
}

/* Remove a new client */
int center_remove_client(int base_id, int client_id)
{
	if (!base_client_list[base_id] || !base_client_list[base_id][client_id] < 0)
	{
		return RTN_GENERAL;
	}
	
	int id = base_client_list[base_id][client_id];

	// Find node
	if (id < global_client_list_size && (base_id == global_client_list[id].base_id) && (client_id == global_client_list[id].client_id))
	{
		mempool_del_client(base_client_list[base_id][client_id]);
		global_client_list[id].base_id = -1;
		global_client_list[id].client_id = -1;

		base_client_list[base_id][client_id] = -1;

		total_clients --;
		total_regoff ++;
		
		fprintf(stderr, "Base %d Client %d removed from global %d\n", base_id, client_id, id);

		return id;
	}

	return RTN_GENERAL;
}

/* Process command */
int center_process_command(int base_id, int client_id, void *data, int len)
{
	char **p;
	unsigned int plen;
	int cmd_id, global_id, i;

	if (len < 4)
	{
		return RTN_GENERAL;
	}

	cmd_id = (int) get_u_int((char *) data);
	
	if (client_id < 0 || client_id > base_client_list_size[base_id] || !base_client_list[base_id] || !base_client_list[base_id][client_id] < 0)
	{
		return RTN_GENERAL;
	}
	
	global_id = base_client_list[base_id][client_id];

	if (global_id < 0)
	{
		return RTN_GENERAL;
	}
	
	p = package_unpack((char *) data + 4, len - 4, &plen);
	runner_go(mempool_get_client_runner(global_id), global_id, cmd_id, p, plen);

	if (p)
	{
		for (i = 0; i < plen; i ++)
		{
			if (p[i])
			{
				free(p[i]);
			}
		}
	
		free(p);
	}
	
	return RTN_SUCCESS;
}

/* Special command (Command ID > 0xFFFFFF00) */
int center_special_command(int base_id, unsigned int cmd_id, unsigned int cmd_val)
{
	if (SGN_HEARTBEAT == cmd_id && 0x0 == cmd_val)
	{
		// Heartbeat
		conn_client_hb(conn_client_list_get(base_id));
	}
	

	else if (SGN_CLIENT_UNREG == cmd_id)
	{
		// Client unregister
		int client_id = (int) cmd_val;

		center_remove_client(base_id, client_id);
	}

	return RTN_SUCCESS;
}

/* Write base info */
int center_store_base_info(int base_id, char *status)
{
	if (base_id >= total_init || base_id < 0)
	{
		return RTN_GENERAL;
	}
	
	base_server_list[base_id].base_id = base_id;

	if (bridge_get_mode() == BRIDGE_MODE_UNITED)
	{
		base_server_list[base_id].base_addr = 0;
	}

	else
	{
		base_server_list[base_id].base_addr = conn_client_list_get(base_id)->client_addr;
	}

	base_server_list[base_id].base_port = (int) get_u_int(status + 8);
	
	if (!base_server_list[base_id].base_version)
	{
		base_server_list[base_id].base_version = strdup(status + 36);
	}

	base_server_list[base_id].start_time = get_u_int(status + 12);
	base_server_list[base_id].current_conn = (int) get_u_int(status + 16);
	base_server_list[base_id].max_conn = (int) get_u_int(status + 20);
	base_server_list[base_id].memory_usage = get_u_int(status + 24);
	base_server_list[base_id].package_read = get_u_int(status + 28);
	base_server_list[base_id].package_send = get_u_int(status + 32);
	
	return base_id;
}

/* Get base info */
struct base_server_info * center_fetch_base_info(int base_id)
{
	if (base_id >= total_init || base_id < 0)
	{
		return (struct base_server_info *) NULL;
	}
	
	return &base_server_list[base_id];
}

/* Get a free global id from list */
int center_get_free_global_id()
{
	int global_id;

	pthread_mutex_lock(&center_lock);
	
	if (max_client_id + 1 < global_client_list_size)
	{
		// Return the nearest id
		global_id = max_client_id + 1;
		max_client_id ++;
		pthread_mutex_unlock(&center_lock);
		
		return global_id;
	}

	else
	{
		// Try loop for empty
		int i;

		for (i = 0; i < global_client_list_size; i ++)
		{
			if (-1 == global_client_list[i].base_id && -1 == global_client_list[i].client_id)
			{
				global_id = i;
				pthread_mutex_unlock(&center_lock);
				
				return global_id;
			}
		}

		// No empty slot~~~ enlarge list
		struct global_client_item *tmp = realloc(global_client_list, 2 * global_client_list_size * sizeof(struct global_client_item));

		if (tmp)
		{
			// Memory pool enlarged successfully
			memset(tmp + global_client_list_size * sizeof(struct global_client_item), 0xFF, global_client_list_size * sizeof(struct global_client_item));
			global_id = global_client_list_size;
			global_client_list_size *= 2;
			global_client_list = tmp;
			pthread_mutex_unlock(&center_lock);

			return global_id;
		}

		else
		{
			// -_-!!! What happened???
			pthread_mutex_unlock(&center_lock);
			
			return RTN_GENERAL;
		}
	}
}

/* Center status */
int center_count_client()
{
	return total_clients;
}

int center_count_regon()
{
	return total_regon;
}

int center_count_regoff()
{
	return total_regoff;
}
